Skip to content

fix(bitbox): guard + self-heal empty wallet address (grey screen)#710

Merged
TaprootFreak merged 1 commit into
stagingfrom
fix/bitbox-empty-address-self-heal
Jun 9, 2026
Merged

fix(bitbox): guard + self-heal empty wallet address (grey screen)#710
TaprootFreak merged 1 commit into
stagingfrom
fix/bitbox-empty-address-self-heal

Conversation

@TaprootFreak

Copy link
Copy Markdown
Contributor

Problem

A BitBox (hardware) wallet could be persisted with an empty on-chain address. On the next app launch — after PIN entry — the dashboard build reads that address through EthereumAddress.fromHex(""), which throws. In release this is uncaught in the build phase and surfaces as a bare grey screen (the default ErrorWidget). Software wallets are unaffected because they always derive a real address.

Root cause

bitbox_flutter's getETHAddress coerces a native null into "" at the transport boundary (bitbox_usb_method_channel.dartreturn result ?? '';; the iOS/Android handlers return it unvalidated). When the device isn't fully ready (e.g. a transient BLE stall right after channel-hash verify), the address comes back empty, and createBitboxWallet persisted it with no validation — this gap has existed since BitBox support was first added. The pairing ceremony also fetched the address with no device-ready re-check or retry (unlike the existing channel-hash retry loop).

What changed

  • Central retrying boundaryBitboxService.getEthAddress never returns empty: a transient empty read self-recovers across bounded retries; a persistent one throws the new typed BitboxAddressUnavailableException.
  • Validate before persistcreateBitboxWallet (and the heal path) route through that boundary and keep a format guard, so an empty/invalid address can never land on disk again. A failed fetch falls back to the pairing flow's existing retry path.
  • Self-heal for already-corrupted wallets — at load, a BitBox row with an empty/invalid address is detected (non-throwing) and the app diverts to a re-pairing recovery page that re-derives and backfills the address onto the existing row, then continues to the dashboard. This is local key derivation (no API state). Cancelling removes the unusable view-wallet (keys live on the device; re-pairing re-derives the same address) so the user is never stranded.
  • Defense-in-depth — a custom ErrorWidget.builder replaces the silent grey box with a logged, on-brand surface, and routes uncaught build errors through FlutterError.onError.

Test plan

  • flutter analyze clean
  • flutter test --exclude-tags golden — full suite green (2334 tests)
  • Unit: createBitboxWallet rejects empty/invalid without persisting; getEthAddress retry (first-ok / empty-then-ok via fakeAsync / persistent-empty throws); currentWalletNeedsAddressRecovery matrix; healCurrentBitboxAddress happy + throw
  • Bloc: HomeBloc diverts to recovery and clears the flag after a clean load
  • Widget: recovery onCancel does not throw on a single-entry stack (+ regression guard)
  • On-device: re-pair an empty-address BitBox wallet → lands on dashboard; cancel → onboarding

A BitBox wallet could be persisted with an empty on-chain address: the
bitbox_flutter transport coerces a native null into "" (result ?? '') when
the device isn't fully ready right after channel-hash verify, and
createBitboxWallet stored it without validation. On the next launch the
dashboard build read that address through EthereumAddress.fromHex("") which
throws, surfacing in release as a bare grey ErrorWidget. Software wallets are
unaffected because they always derive a real address.

- BitboxService.getEthAddress: single retrying boundary that never returns
  empty — a transient empty read self-recovers across attempts, a persistent
  one throws BitboxAddressUnavailableException.
- createBitboxWallet / healCurrentBitboxAddress route through it and keep a
  format guard before persisting.
- Self-heal: detect a BitBox row with an empty/invalid address at load and
  divert to a re-pairing recovery page that re-derives and backfills the
  address (local key derivation, no API state), then continues to the
  dashboard. Cancel removes the unusable view-wallet so the user is never
  stranded.
- Defense-in-depth: a custom ErrorWidget.builder replaces the silent grey box
  with a logged, on-brand surface.
@TaprootFreak TaprootFreak marked this pull request as ready for review June 9, 2026 11:57
@TaprootFreak TaprootFreak merged commit b529dcc into staging Jun 9, 2026
12 checks passed
@TaprootFreak TaprootFreak deleted the fix/bitbox-empty-address-self-heal branch June 9, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant